/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2015, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 * For questions related to commercial use licensing, please contact sales@telestax.com.
 *
 */

package org.restcomm.android.sdk.SignalingClient;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import org.restcomm.android.sdk.RCClient;
import org.restcomm.android.sdk.RCDeviceListener;
import org.restcomm.android.sdk.util.RCException;
import org.restcomm.android.sdk.util.RCLogger;

import java.util.HashMap;

/**
 * SignalingClient provides asynchronous access to lower level signaling facilities. Requests are sent typically from UI thread via methods
 * like open(), close(), etc towards signaling thread. Responses are received via Handler.handleMessage() (since this class is also a Handler)
 * from signaling thread and sent for further processing to SignalingClientListener listener for register/configuration specific
 * functionality and to SignalingClientCallListener listener for call related functionality. Hence, users of this API should implement
 * those and properly handle responses and events.
 *
 * Each request towards the signaling thread is sent together with a unique jobId that identifies this 'job' until it is either complete
 * or an error occurs. Each reply/event associated with this request carries the same jobId, so that the App can correlate them. The lifetime
 * of the job depends on its type. For example for open() jobs it is until the signaling facilities are properly initialized or we get an error
 * at which point we get a onCloseReply() conveying either of the facts. For a call() job the jobId is sent back and forth until the call
 * is disconnected or an error occurs.
 *
 * Notice that some callbacks are called on*Reply() (as opposed to on*Event()). The convention is that those are directly associated
 * to a a simple request (typically non call related) and also carry error status (and possibly connectivity status). In contrast,
 * for call-related functionality separate callbacks are defined for a. success and b. for error codes, since they are much more complicated.
 *
 * The whole architecture in a nutshell is as follows. The Android App or a higher level API (like RCDevice/RCConnection) use the SignalingClient API
 * which underneath creates signaling messages (see SignalingMessage for structure of the messages) and sends them to the Signaling thread
 * that handles them by dispatching them to JainSipClient that encapsulates JAIN SIP. When a response/event comes in from JAIN SIP to JainSipClient
 * the reverse happens: a message is created from the Signaling thread to the UI thread and after it is received at handleMessage() the respective
 * listener callback is used to notify the UI.
 *
 * Note: Although it would make sense to make SignalingClient static in order to accommodate some improvements over the current design, the problem is that
 * we need an object to extend Handler and we definitely need SignalingClient to also be a Handler.
 */
public class SignalingClient extends Handler {

   /**
    * Registration/configuration related interface callbacks that user of the API needs to implement
    */

   public interface SignalingClientListener {
      // Replies
      void onOpenReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text);

      void onCloseReply(String jobId, RCClient.ErrorCodes status, String text);

      void onReconfigureReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text);

      void onMessageReply(String jobId, RCClient.ErrorCodes status, String text);

      // Unsolicited Events
      void onCallArrivedEvent(String jobId, String peer, String sdpOffer, HashMap<String, String> customHeaders);

      void onMessageArrivedEvent(String jobId, String peer, String messageText);

      void onErrorEvent(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text);

      void onConnectivityEvent(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus);

      // Event to convey trying to Register, so that UI can convey that to user (typically by changing RCDevice state to Offline, until register response arrives)
      void onRegisteringEvent(String jobId);

      // TODO: this should be removed after we remodel the whole connection/device communication
      // Call related events that are delegated to RCConnection
      //void onCallRelatedMessage(SignalingMessage message);

      // this is not a callback but we want the listener to implement it so that we cat retrieve the connection from the jobId
      SignalingClient.SignalingClientCallListener getConnectionByJobId(String jobId);
   }

   /**
    * Call related interface callbacks that user of the API needs to implement
    */
   public interface SignalingClientCallListener {
      void onCallOutgoingConnectedEvent(String jobId, String sdpAnswer, HashMap<String, String> customHeaders);

      void onCallIncomingConnectedEvent(String jobId);

      // peer disconnected the call
      void onCallPeerDisconnectEvent(String jobId);

      // peer ringing for outgoing call
      void onCallOutgoingPeerRingingEvent(String jobId);

      // call was disconnected due to local disconnect() call
      void onCallLocalDisconnectedEvent(String jobId);

      // when a call error occurs, we can assume that the call has been killed and the App doesn't have to do anything like hanging it up. The signaling facilities take care of proper call
      // termination
      void onCallErrorEvent(String jobId, RCClient.ErrorCodes status, String text);

      // cancel was was answered for incoming call
      void onCallIncomingCanceledEvent(String jobId);

      void onCallSentDigitsEvent(String jobId, RCClient.ErrorCodes statusCode, String statusText);
   }

   // ------ Not used yet, we 'll use it when we introduce the new messaging API
   public interface UIMessageListener {
      void onMessageSentEvent(String jobId);
   }


   //private static final SignalingClient instance = new SignalingClient();
   SignalingClientListener listener;
   private static final String TAG = "SignalingClient";

   // handler at signaling thread to send messages to
   private SignalingHandlerThread signalingHandlerThread;
   private Handler signalingHandler;
   //UIHandler uiHandler;
   private Context context;
   //HashMap<String, RCMessage> messages;
   //private boolean closePending = false;
   // 10 second timeout for SignalingClient.close()
   //static private final int CLOSE_TIMEOUT = 10000;
   private static boolean initialized = false;

   // private constructor to avoid client applications to use constructor
   public SignalingClient() throws RCException
   {
      super();

      if (initialized) {
         throw new RCException(RCClient.ErrorCodes.ERROR_DEVICE_SIGNALING_FACILITIES_ALREADY_INITIALIZED);
      }

      // create signaling handler thread and handler/signal
      signalingHandlerThread = new SignalingHandlerThread(this);
      signalingHandler = signalingHandlerThread.getHandler();

      initialized = true;
   }

/*
   public static SignalingClient getInstance()
   {
      return instance;
   }
*/

   /**
    * Initialize the signaling facilities
    * @param listener Listener to register/configuration specific events
    * @param context Android context needed by signaling thread
    * @param parameters A map of parameters of the open (TODO: add doc for specific keys)
    * @return The jobId for the new job created at Signaling thread
    */
   public String open(SignalingClientListener listener, Context context, HashMap<String, Object> parameters)
   {
      //uiHandler = new UIHandler(listener);
      this.context = context;
      this.listener = listener;

      String jobId = generateId();
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.OPEN_REQUEST);
      signalingMessage.setParameters(parameters);
      signalingMessage.setAndroidContext(context);

      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();
      return jobId;
   }

   /**
    * Change signaling configuration, like update username/password, change domain, etc
    * @param parameters Reconfigure paramemeters
    * @return The jobId for the new job created at Signaling thread
    */
   public String reconfigure(HashMap<String, Object> parameters)
   {
      String jobId = generateId();
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.RECONFIGURE_REQUEST);
      signalingMessage.setParameters(parameters);

      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();

      return jobId;
   }

   // -- Call related methods. For these the jobId is already generated by the application

   /**
    * Make a call towards a peer
    * @param jobId Unique identifier to identify future replies & events
    * @param parameters Call parameters
    */
   public void call(String jobId, HashMap<String, Object> parameters)
   {
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_REQUEST);
      signalingMessage.setParameters(parameters);
      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();
   }

   /**
    * Accept a call from a peer
    * @param jobId Unique identifier to identify future replies & events
    * @param parameters Accept parameters
    */
   public void accept(String jobId, HashMap<String, Object> parameters)
   {
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_ACCEPT_REQUEST);
      signalingMessage.setParameters(parameters);
      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();
   }

   /**
    * Disconnect a call with a pper
    * @param jobId Unique identifier to identify future replies & events
    * @param reason Reason for the disconnect. If this is a normal disconnect triggered by the user, this is null or empty. But if this is caused because media
    *               connectivity has been severed, then 'reason' conveys the reason and is added as a SIP header to the generated BYE.
    */
   public void disconnect(String jobId, String reason)
   {
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_DISCONNECT_REQUEST);
      signalingMessage.reason = reason;
      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();
   }

   /**
    * Send DTMF digits to peer over existing call
    * @param jobId Unique identifier to identify future replies & events
    * @param digits DTMF digits to send (Important: for now we only support a single digit per sendDigits() call)
    */
   public void sendDigits(String jobId, String digits)
   {
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_SEND_DIGITS_REQUEST);
      signalingMessage.dtmfDigits = digits;
      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();
   }

   /**
    * Send text message to peer
    * @param parameters
    * @return
    */
   public String sendMessage(HashMap<String, Object> parameters)
   {
      String jobId = generateId();

      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.MESSAGE_REQUEST);
      signalingMessage.parameters = parameters;
      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();

      //messages.put(jobId, rcMessage);

      return jobId;
   }

   /**
    * Release the signaling facilities
    * @return
    */
   public String close()
   {
      String jobId = generateId();
      SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CLOSE_REQUEST);
      //signalingMessage.setParameters(parameters);

      Message message = signalingHandler.obtainMessage(1, signalingMessage);
      message.sendToTarget();

      /*
      closePending = true;
      new Handler(getLooper()).postDelayed(
              new Runnable() {
                 @Override
                 public void run()
                 {
                    if (closePending) {
                       RCLogger.w(TAG, "close() timeout: signaling thread didn't return after: " + CLOSE_TIMEOUT + "ms. Stopping signaling thread");
                       signalingHandlerThread.quit();
                    }
                 }
              }
              , CLOSE_TIMEOUT);
              */

      return jobId;
   }

   /**
    * Handle incoming messages from signaling thread
    *
    * @param inputMessage incoming signaling message
    */
   @Override
   public void handleMessage(Message inputMessage)
   {
      // Gets the image task from the incoming Message object.
      SignalingMessage message = (SignalingMessage) inputMessage.obj;

      RCLogger.i(TAG, "handleMessage: type: " + message.type + ", jobId: " + message.jobId);

      if (message.type == SignalingMessage.MessageType.OPEN_REPLY && listener != null) {
         listener.onOpenReply(message.jobId, message.connectivityStatus, message.status, message.text);
      }
      else if (message.type == SignalingMessage.MessageType.CLOSE_REPLY) {
         signalingHandlerThread.quit();
         initialized = false;
         //closePending = false;
         if (listener != null) {
            listener.onCloseReply(message.jobId, message.status, message.text);
         }

      }
      else if (message.type == SignalingMessage.MessageType.RECONFIGURE_REPLY) {
         if (listener != null) {
            listener.onReconfigureReply(message.jobId, message.connectivityStatus, message.status, message.text);
         }

      }
      else if (message.type == SignalingMessage.MessageType.ERROR_EVENT && listener != null) {
         listener.onErrorEvent(message.jobId, message.connectivityStatus, message.status, message.text);
      }
      else if (message.type == SignalingMessage.MessageType.CONNECTIVITY_EVENT  && listener != null) {
         listener.onConnectivityEvent(message.jobId, message.connectivityStatus);
      }
      else if (message.type == SignalingMessage.MessageType.MESSAGE_INCOMING_EVENT  && listener != null) {
         listener.onMessageArrivedEvent(message.jobId, message.peer, message.messageText);
      }
      else if (message.type == SignalingMessage.MessageType.MESSAGE_REPLY  && listener != null) {
         /*
         RCMessage rcMessage = messages.get(message.jobId);
         if (rcMessage == null) {
            throw new RuntimeException("No RCMessage matching incoming message jobId: " + message.jobId);
         }

         rcMessage.onMessageSentEvent(message.jobId);
         */
         listener.onMessageReply(message.jobId, message.status, message.text);
      }
      else if (message.type == SignalingMessage.MessageType.REGISTERING_EVENT  && listener != null) {
         listener.onRegisteringEvent(message.jobId);
      }
      // Call related events
      else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_EVENT) {
         listener.onCallArrivedEvent(message.jobId, message.peer, message.sdp, message.customHeaders);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_OUTGOING_CONNECTED_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         // outgoing call is connected (got 200 OK and ACKed it)
         callListener.onCallOutgoingConnectedEvent(message.jobId, message.sdp, message.customHeaders);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_CONNECTED_EVENT) {
         // incoming call connected
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallIncomingConnectedEvent(message.jobId);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_PEER_DISCONNECT_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallPeerDisconnectEvent(message.jobId);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_OUTGOING_PEER_RINGING_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallOutgoingPeerRingingEvent(message.jobId);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_LOCAL_DISCONNECT_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallLocalDisconnectedEvent(message.jobId);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_ERROR_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallErrorEvent(message.jobId, message.status, message.text);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_CANCELED_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallIncomingCanceledEvent(message.jobId);
      }
      else if (message.type == SignalingMessage.MessageType.CALL_SEND_DIGITS_EVENT) {
         SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId);
         callListener.onCallSentDigitsEvent(message.jobId, message.status, message.text);
      }
      else {
         RCLogger.e(TAG, "handleSignalingMessage(): no handler for signaling message");
      }
   }


   // ------ Helpers

   // Generate unique identifier for 'transactions' created by SignalingClient, this can then be used as call-id when it enters JAIN SIP
   private String generateId()
   {
      return Long.toString(System.currentTimeMillis());
   }
}
